Rekurziós agytorna

Ezúttal nézegetünk egy kis rekurziót, ha már múltkor belemelegedtünk az ilyen állásinterjús kérdésekbe. Mindenekelőtt a rekurzióról egy nagyon jó rövid oktatóvideót találtok itt, érdemes mindenkinek megnézni, aki esetleg még idegenkedne a történettől. Nagy vonalakban annyi a lényege, hogy a függvény saját magát hívja meg futtatása során, minden meghívás során egy új értéket átadva a következő lépésnek. Bizonyos esetekben helyettesítheti a hagyományos programozási módszereket, tisztább, egyszerűbb kódolást tesz lehetővé, azonban vigyázni is kell vele, mivel a stack túlcsordulását okozhatjuk. Persze az is fontos, hogy lefixáljuk, hogy meddig fusson a játék, ami nem meglepő, hiszen egy örökké tartó (vagyis a stack megteléséig tartó) futásban találhatjuk magunkat. A rekurzió működéséi elvét a fenti linken található videó nagyon jól elmagyarázza. Lényeg, hogy minden meghívás során a függvény és visszadott értéke a stackbe kerül, egészen pontosan az eggyel korábbi lépésben lefuttatott lépés fölé, hogy aztán a folyamat egy későbbi részében az értékét átadjuk a hívó félnek. Jajj, ez kicsit bonyolultnak hangzik, pedig tényleg nem az. Mivel a fenti videóban van egy tök jó példa is, most ezt az alap szintet átugorjuk és fejest ugrunk egy fokkal megbonyolított történetbe.

A játék a coin challenge nevet viseli, sokszor elő szokott bukkanni interjúkon. Lényege, hogy van megadott típusú fémpénzed (alábbi példában 1 ft-os és 2 ft-os), aminek kombinációival ki kell adnod egy adott összeget (alábbi példában 4-et). Kérdés, hogy hány féle variációja létezik? A konkrét példában 3 lesz a válasz, hiszen:

- 1+1+1+1 = 4

- 2+1+1 = 4

- 2+2 = 4

Nézzük csak a mellékelt kódot! Jujj de bonyinak tűnik, pedig ha megértjük a logikáját, nem lesz az! Van először is a main metódus, ez eléggé tiszta, meghívjuk a calculateCombo eljárásunkat, és átadjuk neki a kétféle forint típust (1-est és 2-est), valamint a végösszeget (4). Van még egy 0 értékű paraméter is, a lényege az, hogy induláskor az array első elemét, az 1 ft-os értékét jelezzük a programunknak vele, meg fogjuk érteni később a logikáját. Az a fontos, hogy azt lássuk, hogy ha a totalAmount értéket nem kezeljük le a programunkban, akkor a végeredmény nem lesz jó (5 lesz 3 helyett), mivel a 2+1+1 megoldás úgy is felírható, hogy 1+2+2, vagy 1+1+2. Igazából mindegyik jó, de nekünk a végeredmény szempontjából a forintok sorrendje lényegtelen.

Oké, nézzük a calculateCombo-t. Van egy combos nevű változónk 0 kezdőértékkel, ebbe fogjuk beletenni a sikeres futások számát, azaz minden sikeres futás esetén 1-el növeljük az értékét. Sikeres futás az, ha az amount értéke 0 lesz, azaz a folyamat során ha kivonunk a 4-ből mondjuk 1-et, a következő lépésben is 1-et, aztán még 1-et, az utolsó lépésben is 1-et, akkor megkapjuk az ötödik lépésben a 0 amount értéket. Ez esetben 1-el növeljük a combos értékét. Ha mondjuk az utolsó előtti lépésben 2-t vonnánk ki, akkor -1-et kapnánk, az ugye nem jó, azt az ágat ugorjuk is a return 0 átadásával.

Csinálunk egy for ciklust, amiben 2 lépést fogunk megtenni, első körben a coins array első elemét, az 1 ft-ost futtatjuk le, majd a folyamat későbbi szakaszában a 2 ft-os értéket. Itt jön képbe a currentIndex értéke, amelyet a későbbi lépésekben 0-ról 1-re állítunk (azaz 1 ft-osról 2 ft-osra), hogy még véletlenül se próbálkozzon a program újra az 1 ft-ossal. Mindjárt mutatok egy ábrát, ami alapján jól meg lehet ezt érteni, hogy miért is jó ez nekünk.

A rekurzió folyamata
A rekurzió folyamata
Nézzük is végig az ábrát, és menjünk lépésenként, egyből rájöttök, hogy mi miért is van a kódban.

- 1. lépés: 0,4,0. Azaz elindítjuk a calculateCombo-t első körben, átadjuk neki a 0-s coin-t (azaz 1 ft-ost), a 4-es amount értéket (azaz induláskor még 4 ft-nál tartunk a 0-ig történő kivonásoknál), és a 0-s currentIndex-et (azaz jelezzük, hogy az 1 ft-ot vizsgáljuk, ez persze szorosan összefügg az első paraméterrel, a kettő értéke ugyanaz lesz minden lépésnél). Szóval az első lépésben átadjuk ezeket a paramétereket, elsőként megvizsgálja a program, hogy az amount értéke 0 lesz-e. Mivel nem, ezért megyünk tovább, mivel nem kisebb mint 0 az amount értéke, megyünk tovább, és eljutunk a for ciklusig. Itt meghívjuk a calculateCombo következő lépését, átadva neki a 0-s coint (1 ft-ost), a 4-1, azaz 3-as amount értéket, illetve továbbra is a 0-s értéket (mivel még a for ciklus első lépésénél vagyunk, azaz az 1 ft-ost vizsgáljuk).

- 2. lépés: 0,3,0. Az első lépés végén tehát a for ciklusban átadtuk ezeket a paremétereket. Mivel továbbra is nagyobb 0-nál az amount értéke, megyünk a for ciklushoz, ahol újra a for ciklus első lépését követjük el (és nem a másodikat, ez egy olyan pont, ami nem mindig egyértelmű, amikor az ember először találkozik ezzel a feladattal). Megívjuk a calculateCombo-t újra, ezúttal 0 coin értékkel (1 ft), 3-1 = 2 amount érékkel, és továbbra is 1 ft-al (azaz 0 értékkel).

- 3-5. lépés: és így megyünk tovább, egészen addig, amíg az amount értéke 0 lesz. Szuper, máris a combos értékét emeljük 1-el! Meg is állunk a return 1-nél, és nem megyünk tovább a for ciklusig, a stack-ből ki is kerül ez a lépés, visszalépünk 1-et a stack-ben, egészen a 4. lépésig.

- 6. lépés: a 4. lépéshez visszatérve a for ciklus második lépése fog lefutni, ezúttal úgy, hogy a calculateCombo-t 1 (azaz 2 ft), 1-2 (azaz -1), valamint 1 (azaz 2 ft) értékkel hívjuk meg. Itt figyeljük meg azt, hogy a currentIndex értéke 1-esre változott, ami a későbbi lépésekben már nem is fog tudni 0-ra visszatérni. Mivel a for ciklusban minden további lépésben a currentIndex értékénél kezdjük a ciklus lépéseit (azaz innentől 1-től), ez azt eredményezi, hogy a további lépésekben a 0-t (azaz 1 ft-ot) már nem fogjuk vizsgálni. Ez azért lesz jó nekünk, mert így megelőzzük a fölösleges köröket, és végeredményként nem 5-öt kapunk majd, hanem 3-at. Mindjárt megértitek miért. Szóval a 6. lépésben megkapta a calculateCombo az 1, -1, 1 értéket, mivel az amount értéke kisebb mint 0, a for ciklusig nem jutunk el, hanem 0 értékkel befejezzük a lépés futását, a stack-ben is törlődik ez a lépés, majd visszalépünk egyet a stack-ben, a 3. lépésig.

- 7. lépés: a 3. lépés for ciklusának 2. köre következik, a calculateCombo-t meghívjuk 1, 2-2 = 0, 1 értékekkel. Mivel az amount = 0, ezért ez is egy sikeres lépés volt, emeljük a combos értékét 1-el. A stack-ből is törlődik ez a lépés, illetve a 3. lépés is, mivel annak mindkét for ágán már végigmentünk, így visszamegyünk a 2. lépésig.

- 8. lépés: itt 1, 3-2 = 1, 1 értékeket kaptunk, így megyünk a for ciklusig. Mivel korábban a currentIndex megkapta az 1-es értéket, ezért itt már csak egyszer fut le a for ciklus, 2 ft-al. Tehát meghívjuk a calculateCombo-t 1, 1-2 = -1, 1 értékekkel.

- 9. lépés: mivel -1 értéket kaptunk, ezért ez az ág is sztornó, a stack-ből törlődik, visszalépünk a 8. lépésig. Mivel ott a for ciklusnak csak egyszer kellett lefutnia, a stack-ből az is törlődik, visszalépünk a 2. lépésig, aminek szintén végigfutott a for ciklusa, így a stack-ből az is kikerül, visszajutunk hát az 1. lépésig.

- 10. lépés: az első lépés for ciklusának 2. lépése fut le 1, 4-2 = 2, 1 értékekkel. Mivel az amount = 2, megyünk a for ciklusig, ahol currentIndex 1-es értéke miatt csak a 2. kör fut le 1, 2-2 = 0, 1 értékekkel.

- 11. lépés: és eljutottunk az utolsó lépésig, mivel az amount értéke 0, a combos értékét növeljük 1-el és 3-at kapunk.

Innen nincs tovább, a stack-ből szépen minden lépés kikerül, a folyamat a végére jutott. Szóval ennyi volt, tényleg nem egy nagy ördőngősség, egyszer kell csak megérteni a folyamatot, onnantól kezdve nem lesz baj az ehhez hasonló feladatokkal.